Based on the exploratory data analysis, we can now start putting together datasets aimed at answering the following question:
What impacted people’s likelihood to buy training in 2017?
library(tidyverse)
library(dbplyr)
Buying training
Who bought training in 2017? We need to grab the data about who bought and also include the students email addresses to account for students being the requesters but not the purchasers. We will need to exclude the free training options.
con %>%
tbl("woocommerce_order_items") ->
items
con %>%
tbl("posts") ->
posts
posts %>%
inner_join(items, by=c("id"="order_id")) %>%
filter(order_item_name!='Free Membership') %>%
select(order_id=id, purchase_date=post_date_gmt, order_item_name, order_item_id) %>%
collect() ->
orders
library(readxl)
"../data/Order Emails.xlsx" %>%
read_excel() %>%
select( order_id=post_id, starts_with("email"))->
orders_purchasers
con %>%
tbl("woocommerce_order_items_meta") %>%
left_join(items) %>%
select( order_id, starts_with("email")) %>%
collect() ->
orders_students
Joining, by = "order_item_id"
orders_purchasers %>%
union(orders_students) %>%
inner_join(orders)->
orders_emails
Joining, by = "order_id"
orders_emails %>%
group_by(email_user_id, email_domain_id) %>%
summarise(orders=n(),
earliest=min(purchase_date),
latest=max(purchase_date),
products=list(order_item_name)) ->
customers
Let’s take a quick look at the distribution of orders by email address.
customers %>%
arrange(desc(orders))
customers %>%
ggplot(aes(x=orders)) +
geom_histogram(bins=20)

With this list of unique email addresses involved in sales we can see how these match up against the membership list. Let’s first look at the full list to see how many people were on the list and when they joined.
con %>%
tbl("mailchimp_members") ->
mailchimp_members
customers %>%
select(-products) %>%
full_join( mailchimp_members, copy=TRUE,
by= c("email_user_id","email_domain_id")) ->
mailchimp_customers
(mailchimp_customers %>%
mutate(purchases=!is.na(orders),
mailchimp=!is.na(member_rating),
signed_after_order=confirm_time>earliest) %>%
group_by(mailchimp,purchases,signed_after_order) %>%
summarise(n=n()) ->
mailchimp_training_overlap)
Looking at the records, 24.9% of customers do not have a mailchimp subscription (currently). 16.1% appear to have become mailchimp members after purchasing. This leaves 59% of customers being on the mailing list before making their first purchase.
mailchimp_customers %>%
filter(!is.na(member_rating)) %>%
mutate(any_purchase=!is.na(orders),
purchase=!is.na(orders)&confirm_time>earliest) %>%
mutate(want_to_gets=str_split(i_want_to_get,", ")) %>%
unnest(want_to_gets) %>%
mutate(want_to_gets=coalesce(want_to_gets,"Unknown recd"), n=TRUE) %>%
spread(want_to_gets, n,fill = FALSE) %>%
mutate(signup_sources=str_split(signup_source,", ")) %>%
unnest(signup_sources) %>%
mutate(signup_sources=coalesce(signup_sources,"Unknown source"), n=TRUE) %>%
spread(signup_sources, n,fill = FALSE) ->
mailchimp_labelled
A basic model
Without taking into behaviour in the run up of purchasing, let’s do a very quick and dirty model to see whether some of the mailchimp member level features are useful as-is for predicting purchases. We’ll use a decision tree based model for this.
library(FFTrees)
O
/ \
F O
/ \
F T
FFTrees v1.3.5. Email: Nathaniel.D.Phillips.is@gmail.com
FFTrees.guide() opens the package guide. Citation info at citation('FFTrees')
mailchimp_labelled %>%
select(-(email_user_id:client_type),-(optin_time:longitude),
-time_zone,-(region:notes), -purchase) %>%
FFTrees(any_purchase ~ ., data=.,
do.comp = FALSE,
train.p = 0.7,
sens.w = 0.75,
decision.labels = c("Didn't purchase","Purchased"),
progress = FALSE) ->
initial_tree
plot(initial_tree)

This is a simple model that correctly predicts a good proportion of the customers that purchased.
initial_tree
FFT #1 predicts any_purchase using 4 cues: {member_rating,Unknown recd,Monday Links - our favorite SQL & tech news from the week,First Responder Kit}
[1] If member_rating > 2, predict Purchased.
[2] If Unknown recd != TRUE, predict Didn't purchase.
[3] If Monday Links - our favorite SQL & tech news from the week != FALSE, predict Didn't purchase.
[4] If First Responder Kit != FALSE, predict Didn't purchase, otherwise, predict Purchased.
train test
cases :n 69363.00 29727.00
speed :mcu 1.93 1.93
frugality :pci 0.94 0.94
accuracy :acc 0.65 0.65
weighted :wacc 0.75 0.74
sensitivity :sens 0.78 0.77
specificity :spec 0.65 0.65
pars: algorithm = 'ifan', goal = 'wacc', goal.chase = 'bacc', sens.w = 0.75, max.levels = 4
This model heavily emphasised correctly identifying the people who would purchase at the expense of including more people who wouldn’t purchase. We get a get a high degree of sensitivity (correctly predicting those who purchased) from the mailchimp engagement figure, people ticking what types of emails they want to receive and them not wanting the Monday email.
This model will be our baseline when we get into building more models.
Opens
Now that we have for each person on the mailing list, whether they made a purchase or not, we should build some features around how often people have opened the newsletters in the past.
library(padr)
library(timeDate)
mailchimp_labelled %>%
thicken("week","start_week",by="confirm_time") %>%
mutate(earliest=coalesce(earliest, as.POSIXct("2017-12-31"))) %>%
thicken("week","sale_week",by="earliest") %>%
select(email_user_id, start_week, sale_week) ->
chimp_lite
opens <- dbGetQuery(con,
"select
email_user_id,
count(*) as n
from anon.mailchimp_opens o
group by email_user_id,
CONVERT(date, o.click_timestamp)")
as.Date("2011-01-01") %>%
seq.Date(as.Date("2018-01-01"), 1) ->
dates_seq
dates_seq %>%
format() %>%
timeDate() ->
dates_timeDate
holiday<-isHoliday(dates_timeDate,holidayNYSE(2011:2018))
weekday<-isWeekday(dates_timeDate)
dates_lookup<-tibble(click_timestamp=dates_seq,
holiday,
weekday,
weekend=!weekday)
opens %>%
left_join(dates_lookup)->
opens
Joining, by = "click_timestamp"
opens %>%
thicken("week", "click") %>%
inner_join(chimp_lite, by="email_user_id") %>%
mutate(start_week_diff=difftime(click, start_week, units="week"),
sale_week_diff=difftime(sale_week, click, units="week")) %>%
mutate(start_month=(as.integer(start_week_diff) %/% 4) +1,
sale_month=(as.integer(sale_week_diff) %/% 4) +1 ) ->
opens
opens %>%
sample_n(200)
Opens by usage
(opens %>%
group_by(email_user_id) %>%
summarise(holiday_open_raw=sum(holiday*n),
holiday_open_prop=holiday_open_raw/sum(n),
weekday_open_raw=sum(weekday*n),
weekday_open_prop=weekday_open_raw/sum(n),
weekend_open_raw=sum(weekend*n),
weekend_open_prop=weekend_open_raw/sum(n)
) ->
opens_typeofday)
Opens over time
opens %>%
filter(start_month > 0) %>%
mutate(since_start_h=start_month %/% 6) %>%
group_by(email_user_id,since_start_h) %>%
summarise(opens=sum(n),active=TRUE) %>%
group_by(email_user_id) %>%
mutate(open_prop=opens/sum(opens),
since_start_h=paste0("h",since_start_h)) ->
opens_start_hl
opens_start_hl %>%
mutate(since_start_h=paste0(since_start_h,"_opens")) %>%
spread(since_start_h, opens, fill=0) ->
opens_start_opens
opens_start_hl %>%
select(email_user_id:since_start_h, active) %>%
mutate(since_start_h=paste0(since_start_h,"_opensactive")) %>%
spread(since_start_h, active, fill=FALSE) ->
opens_start_active
opens_start_hl %>%
select(email_user_id:since_start_h, open_prop) %>%
mutate(since_start_h=paste0(since_start_h,"_opensprop")) %>%
spread(since_start_h, open_prop, fill=0) ->
opens_start_prop
(opens_start_opens %>%
left_join(opens_start_active) %>%
left_join(opens_start_prop) ->
opens_activity)
rm(list=c("opens","opens_start_hl",
"opens_start_opens","opens_start_active",
"opens_start_prop"))
Clicks
clicks <- dbGetQuery(con,
"select
email_user_id,
CONVERT(date, o.click_timestamp) as click_timestamp,
o.click_url,
count(*) as n
from anon.mailchimp_clicks o
group by email_user_id,
CONVERT(date, o.click_timestamp),
o.click_url")
clicks %>%
left_join(dates_lookup)->
clicks
clicks %>%
thicken("week", "click") %>%
inner_join(chimp_lite, by="email_user_id") %>%
mutate(start_week_diff=difftime(click, start_week, units="week"),
sale_week_diff=difftime(sale_week, click, units="week")) %>%
mutate(start_month=(as.integer(start_week_diff) %/% 4) +1,
sale_month=(as.integer(sale_week_diff) %/% 4) +1 ) ->
clicks
Clicks by usage
(clicks %>%
group_by(email_user_id) %>%
summarise(holiday_clicks_raw=sum(holiday*n),
holiday_clicks_prop=holiday_clicks_raw/sum(n),
weekday_clicks_prop=weekday_clicks_raw/sum(n),
weekend_clicks_raw=sum(weekend*n),
weekend_clicks_prop=weekend_clicks_raw/sum(n)
) ->
clicks_typeofday)
Clicks over time
clicks %>%
filter(start_month > 0) %>%
mutate(since_start_h=start_month %/% 6) %>%
group_by(email_user_id,since_start_h) %>%
summarise(clicks=sum(n),active=TRUE) %>%
group_by(email_user_id) %>%
mutate(click_prop=clicks/sum(clicks),
since_start_h=paste0("h",since_start_h)) ->
clicks_start_hl
clicks_start_hl %>%
select(email_user_id:since_start_h, clicks) %>%
mutate(since_start_h=paste0(since_start_h,"_clicks")) %>%
spread(since_start_h, clicks, fill=0) ->
clicks_start_clicks
clicks_start_hl %>%
select(email_user_id:since_start_h, active) %>%
mutate(since_start_h=paste0(since_start_h,"_clickactive")) %>%
spread(since_start_h, active, fill=FALSE) ->
clicks_start_active
clicks_start_hl %>%
select(email_user_id:since_start_h, click_prop) %>%
mutate(since_start_h=paste0(since_start_h,"_clickprop")) %>%
spread(since_start_h, click_prop, fill=0) ->
clicks_start_prop
(clicks_start_clicks %>%
left_join(clicks_start_active) %>%
left_join(clicks_start_prop) ->
click_activity)
rm(list=c("clicks_start_hl",
"clicks_start_clicks","clicks_start_active",
"clicks_start_prop"))
Clicks by topic
For how we arrived at topics per url, check out What topics did people click?
clicks %>%
inner_join({
"../data/link_lookup.csv" %>%
read_csv()
}, by="click_url") %>%
left_join({
"../data/urltopics.csv"%>%
read_csv()
}, by=c("clean_file"="url")) %>%
mutate(brent=ifelse(str_detect(clean_url,"brentozar"),"B","nB")) %>%
count( email_user_id, brent, topic) %>%
unite(topic_brent,topic, brent) %>%
mutate(topic_brent=paste0("topic_",topic_brent)) %>%
group_by(email_user_id) %>%
mutate(prop=nn/sum(nn)) ->
topic_clicks_l
topic_clicks_l %>%
select(-prop) %>%
mutate(topic_brent=paste0(topic_brent,"_clicks")) %>%
topic_clicks_clicks
topic_clicks_l %>%
select(-nn) %>%
mutate(topic_brent=paste0(topic_brent,"_prop")) %>%
spread(topic_brent, prop, fill=0) ->
topic_clicks_prop
topic_clicks_l %>%
mutate(topic_brent=paste0(topic_brent,"_active")) %>%
group_by(topic_brent,email_user_id) %>%
summarise(active=1) %>%
spread(topic_brent, active, fill=0) ->
topic_clicks_active
(topic_clicks_clicks %>%
inner_join(topic_clicks_prop) %>%
inner_join(topic_clicks_active)->
topic_clicks)
rm(list=c("topics_clicks_l",
"topic_clicks_clicks","topic_clicks_prop",
"topic_clicks_active",
"clicks"))
LS0tDQp0aXRsZTogIkdldHRpbmcgYWxsIHVwIGluIEJyZW50J3MgYml6IC0gRmVhdHVyZSBlbmdpbmVlcmluZyINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIHRvYzogeWVzDQogICAgdG9jX2RlcHRoOiAxDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQod2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQpsaWJyYXJ5KERCSSkNCmNvbiA8LSBkYkNvbm5lY3Qob2RiYzo6b2RiYygpLCAuY29ubmVjdGlvbl9zdHJpbmcgPSAiRHJpdmVyPXtPREJDIERyaXZlciAxMyBmb3IgU1FMIFNlcnZlcn07c2VydmVyPXticmVudG8uZGF0YWJhc2Uud2luZG93cy5uZXR9Ow0KZGF0YWJhc2U9e2JyZW50b2RifTsNCnVpZD17ZGF0YXNjaX07DQpwd2Q9e25aWTAqNTFsR159OyIpDQoNCiMgY3JlZCBjcmVhdGlvbg0KDQojaW4gbWFzdGVyDQojQ1JFQVRFIExPR0lOIGRhdGFzY2kgV0lUSCBwYXNzd29yZD0nblpZMCo1MWxHXic7DQojIGluIGRiDQojQ1JFQVRFIFVTRVIgZGF0YXNjaSBGUk9NIExPR0lOIGRhdGFzY2k7DQojRVhFQyBzcF9hZGRyb2xlbWVtYmVyICdkYl9kYXRhcmVhZGVyJywgJ2RhdGFzY2knOw0KI2FsdGVyIHVzZXIgZGF0YXNjaSB3aXRoIGRlZmF1bHRfc2NoZW1hPWFub24NCmBgYA0KDQpCYXNlZCBvbiBbdGhlIGV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXNdKGJyZW50c2JpekVEQS5odG1sKSwgd2UgY2FuIG5vdyBzdGFydCBwdXR0aW5nIHRvZ2V0aGVyIGRhdGFzZXRzIGFpbWVkIGF0IGFuc3dlcmluZyB0aGUgZm9sbG93aW5nIHF1ZXN0aW9uOg0KDQo+IFdoYXQgaW1wYWN0ZWQgcGVvcGxlJ3MgbGlrZWxpaG9vZCB0byBidXkgdHJhaW5pbmcgaW4gMjAxNz8NCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgcmVzdWx0cz0naGlkZSd9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoZGJwbHlyKQ0KYGBgDQoNCiMgQnV5aW5nIHRyYWluaW5nDQoNCldobyBib3VnaHQgdHJhaW5pbmcgaW4gMjAxNz8gV2UgbmVlZCB0byBncmFiIHRoZSBkYXRhIGFib3V0IHdobyBib3VnaHQgYW5kIGFsc28gaW5jbHVkZSB0aGUgc3R1ZGVudHMgZW1haWwgYWRkcmVzc2VzIHRvIGFjY291bnQgZm9yIHN0dWRlbnRzIGJlaW5nIHRoZSByZXF1ZXN0ZXJzIGJ1dCBub3QgdGhlIHB1cmNoYXNlcnMuIFdlIHdpbGwgbmVlZCB0byBleGNsdWRlIHRoZSBmcmVlIHRyYWluaW5nIG9wdGlvbnMuDQoNCmBgYHtyfQ0KY29uICU+JSANCiAgdGJsKCJ3b29jb21tZXJjZV9vcmRlcl9pdGVtcyIpIC0+DQogIGl0ZW1zDQoNCmNvbiAlPiUgDQogIHRibCgicG9zdHMiKSAtPg0KICBwb3N0cw0KDQpwb3N0cyAlPiUgDQogIGlubmVyX2pvaW4oaXRlbXMsIGJ5PWMoImlkIj0ib3JkZXJfaWQiKSkgJT4lIA0KICBmaWx0ZXIob3JkZXJfaXRlbV9uYW1lIT0nRnJlZSBNZW1iZXJzaGlwJykgJT4lIA0KICBzZWxlY3Qob3JkZXJfaWQ9aWQsIHB1cmNoYXNlX2RhdGU9cG9zdF9kYXRlX2dtdCwgb3JkZXJfaXRlbV9uYW1lLCBvcmRlcl9pdGVtX2lkKSAlPiUgDQogIGNvbGxlY3QoKSAtPg0KICBvcmRlcnMNCg0KbGlicmFyeShyZWFkeGwpDQoiLi4vZGF0YS9PcmRlciBFbWFpbHMueGxzeCIgJT4lIA0KICByZWFkX2V4Y2VsKCkgJT4lIA0KICBzZWxlY3QoIG9yZGVyX2lkPXBvc3RfaWQsIHN0YXJ0c193aXRoKCJlbWFpbCIpKS0+DQogIG9yZGVyc19wdXJjaGFzZXJzDQoNCmNvbiAlPiUgDQogIHRibCgid29vY29tbWVyY2Vfb3JkZXJfaXRlbXNfbWV0YSIpICU+JSANCiAgbGVmdF9qb2luKGl0ZW1zKSAlPiUgDQogIHNlbGVjdCggb3JkZXJfaWQsIHN0YXJ0c193aXRoKCJlbWFpbCIpKSAlPiUgDQogIGNvbGxlY3QoKSAtPg0KICBvcmRlcnNfc3R1ZGVudHMNCg0Kb3JkZXJzX3B1cmNoYXNlcnMgJT4lIA0KICB1bmlvbihvcmRlcnNfc3R1ZGVudHMpICU+JSANCiAgaW5uZXJfam9pbihvcmRlcnMpLT4NCiAgb3JkZXJzX2VtYWlscw0KDQpvcmRlcnNfZW1haWxzICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCwgZW1haWxfZG9tYWluX2lkKSAlPiUgDQogIHN1bW1hcmlzZShvcmRlcnM9bigpLA0KICAgICAgICAgICAgZWFybGllc3Q9bWluKHB1cmNoYXNlX2RhdGUpLA0KICAgICAgICAgICAgbGF0ZXN0PW1heChwdXJjaGFzZV9kYXRlKSwNCiAgICAgICAgICAgIHByb2R1Y3RzPWxpc3Qob3JkZXJfaXRlbV9uYW1lKSkgLT4NCiAgY3VzdG9tZXJzDQpgYGANCg0KTGV0J3MgdGFrZSBhIHF1aWNrIGxvb2sgYXQgdGhlIGRpc3RyaWJ1dGlvbiBvZiBvcmRlcnMgYnkgZW1haWwgYWRkcmVzcy4NCg0KYGBge3J9DQpjdXN0b21lcnMgJT4lIA0KICBhcnJhbmdlKGRlc2Mob3JkZXJzKSkNCmBgYA0KDQpgYGB7cn0NCmN1c3RvbWVycyAlPiUgIA0KICBnZ3Bsb3QoYWVzKHg9b3JkZXJzKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShiaW5zPTIwKQ0KYGBgDQoNCldpdGggdGhpcyBsaXN0IG9mIHVuaXF1ZSBlbWFpbCBhZGRyZXNzZXMgaW52b2x2ZWQgaW4gc2FsZXMgd2UgY2FuIHNlZSBob3cgdGhlc2UgbWF0Y2ggdXAgYWdhaW5zdCB0aGUgbWVtYmVyc2hpcCBsaXN0LiBMZXQncyBmaXJzdCBsb29rIGF0IHRoZSBmdWxsIGxpc3QgdG8gc2VlIGhvdyBtYW55IHBlb3BsZSB3ZXJlIG9uIHRoZSBsaXN0IGFuZCB3aGVuIHRoZXkgam9pbmVkLg0KDQoNCmBgYHtyfQ0KY29uICU+JSANCiAgdGJsKCJtYWlsY2hpbXBfbWVtYmVycyIpIC0+DQogIG1haWxjaGltcF9tZW1iZXJzDQoNCmN1c3RvbWVycyAlPiUgDQogIHNlbGVjdCgtcHJvZHVjdHMpICU+JSANCiAgZnVsbF9qb2luKCBtYWlsY2hpbXBfbWVtYmVycywgY29weT1UUlVFLCANCiAgICAgICAgICAgICBieT0gYygiZW1haWxfdXNlcl9pZCIsImVtYWlsX2RvbWFpbl9pZCIpKSAtPg0KICBtYWlsY2hpbXBfY3VzdG9tZXJzDQoNCihtYWlsY2hpbXBfY3VzdG9tZXJzICU+JSANCiAgbXV0YXRlKHB1cmNoYXNlcz0haXMubmEob3JkZXJzKSwNCiAgICAgICAgIG1haWxjaGltcD0haXMubmEobWVtYmVyX3JhdGluZyksDQogICAgICAgICBzaWduZWRfYWZ0ZXJfb3JkZXI9Y29uZmlybV90aW1lPmVhcmxpZXN0KSAlPiUgDQogIGdyb3VwX2J5KG1haWxjaGltcCxwdXJjaGFzZXMsc2lnbmVkX2FmdGVyX29yZGVyKSAlPiUgDQogIHN1bW1hcmlzZShuPW4oKSkgLT4NCiAgbWFpbGNoaW1wX3RyYWluaW5nX292ZXJsYXApDQpgYGANCg0KYGBge3IgZWNobz1GQUxTRX0NCm1haWxjaGltcF90cmFpbmluZ19vdmVybGFwICU+JSANCiAgZmlsdGVyKG1haWxjaGltcCxwdXJjaGFzZXMsICFzaWduZWRfYWZ0ZXJfb3JkZXIpICU+JSANCiAgcHVsbChuKSAlPiUgDQogIHN1bSgpIC0+IA0KICBtYWlsY2hpbXBfYW5kX3B1cmNoYXNlZA0KDQptYWlsY2hpbXBfdHJhaW5pbmdfb3ZlcmxhcCAlPiUgDQogIGZpbHRlcighbWFpbGNoaW1wLHB1cmNoYXNlcykgJT4lIA0KICBwdWxsKG4pICU+JSANCiAgc3VtKCkgLT4gDQogIG5vbWFpbGNoaW1wX2FuZF9wdXJjaGFzZWQNCg0KDQptYWlsY2hpbXBfdHJhaW5pbmdfb3ZlcmxhcCAlPiUgDQogIGZpbHRlcihzaWduZWRfYWZ0ZXJfb3JkZXIpICU+JSANCiAgcHVsbChuKSAlPiUgDQogIHN1bSgpIC0+IA0KICBtYWlsY2hpbXBfYW5kX3B1cmNoYXNlZF9lYXJsaWVyDQpgYGANCg0KTG9va2luZyBhdCB0aGUgcmVjb3JkcywgYHIgc2NhbGVzOjpwZXJjZW50KG5vbWFpbGNoaW1wX2FuZF9wdXJjaGFzZWQvbnJvdyhjdXN0b21lcnMpKWAgb2YgY3VzdG9tZXJzIGRvIG5vdCBoYXZlIGEgbWFpbGNoaW1wIHN1YnNjcmlwdGlvbiAoY3VycmVudGx5KS4gYHIgc2NhbGVzOjpwZXJjZW50KG1haWxjaGltcF9hbmRfcHVyY2hhc2VkX2VhcmxpZXIvbnJvdyhjdXN0b21lcnMpKWAgYXBwZWFyIHRvIGhhdmUgYmVjb21lIG1haWxjaGltcCBtZW1iZXJzIGFmdGVyIHB1cmNoYXNpbmcuIFRoaXMgbGVhdmVzIGByIHNjYWxlczo6cGVyY2VudChtYWlsY2hpbXBfYW5kX3B1cmNoYXNlZC9ucm93KGN1c3RvbWVycykpYCBvZiBjdXN0b21lcnMgYmVpbmcgb24gdGhlIG1haWxpbmcgbGlzdCAqYmVmb3JlKiBtYWtpbmcgdGhlaXIgZmlyc3QgcHVyY2hhc2UuIA0KDQpgYGB7cn0NCm1haWxjaGltcF9jdXN0b21lcnMgJT4lIA0KICBmaWx0ZXIoIWlzLm5hKG1lbWJlcl9yYXRpbmcpKSAlPiUgDQogIG11dGF0ZShhbnlfcHVyY2hhc2U9IWlzLm5hKG9yZGVycyksDQogICAgICAgICBwdXJjaGFzZT0haXMubmEob3JkZXJzKSZjb25maXJtX3RpbWU+ZWFybGllc3QpICAlPiUgDQogIG11dGF0ZSh3YW50X3RvX2dldHM9c3RyX3NwbGl0KGlfd2FudF90b19nZXQsIiwgIikpICU+JSANCiAgdW5uZXN0KHdhbnRfdG9fZ2V0cykgJT4lIA0KICBtdXRhdGUod2FudF90b19nZXRzPWNvYWxlc2NlKHdhbnRfdG9fZ2V0cywiVW5rbm93biByZWNkIiksIG49VFJVRSkgJT4lIA0KICBzcHJlYWQod2FudF90b19nZXRzLCBuLGZpbGwgPSBGQUxTRSkgJT4lIA0KICBtdXRhdGUoc2lnbnVwX3NvdXJjZXM9c3RyX3NwbGl0KHNpZ251cF9zb3VyY2UsIiwgIikpICU+JSANCiAgdW5uZXN0KHNpZ251cF9zb3VyY2VzKSAlPiUgDQogIG11dGF0ZShzaWdudXBfc291cmNlcz1jb2FsZXNjZShzaWdudXBfc291cmNlcywiVW5rbm93biBzb3VyY2UiKSwgbj1UUlVFKSAlPiUgDQogIHNwcmVhZChzaWdudXBfc291cmNlcywgbixmaWxsID0gRkFMU0UpIC0+DQogIG1haWxjaGltcF9sYWJlbGxlZA0KYGBgDQoNCiMgQSBiYXNpYyBtb2RlbA0KV2l0aG91dCB0YWtpbmcgaW50byBiZWhhdmlvdXIgaW4gdGhlIHJ1biB1cCBvZiBwdXJjaGFzaW5nLCBsZXQncyBkbyBhIHZlcnkgcXVpY2sgYW5kIGRpcnR5IG1vZGVsIHRvIHNlZSB3aGV0aGVyIHNvbWUgb2YgdGhlIG1haWxjaGltcCBtZW1iZXIgbGV2ZWwgZmVhdHVyZXMgYXJlIHVzZWZ1bCBhcy1pcyBmb3IgcHJlZGljdGluZyBwdXJjaGFzZXMuIFdlJ2xsIHVzZSBhIGRlY2lzaW9uIHRyZWUgYmFzZWQgbW9kZWwgZm9yIHRoaXMuDQoNCmBgYHtyIGZpZy5oZWlnaHQ9MTV9DQpsaWJyYXJ5KEZGVHJlZXMpDQoNCm1haWxjaGltcF9sYWJlbGxlZCAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIHNlbGVjdCgtKGVtYWlsX3VzZXJfaWQ6Y2xpZW50X3R5cGUpLC0ob3B0aW5fdGltZTpsb25naXR1ZGUpLA0KICAgICAgICAgLXRpbWVfem9uZSwtKHJlZ2lvbjpub3RlcyksIC1wdXJjaGFzZSkgJT4lIA0KICBGRlRyZWVzKGFueV9wdXJjaGFzZSB+IC4sIGRhdGE9LiwNCiAgICAgICAgICAgICAgICAgICBkby5jb21wID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgdHJhaW4ucCA9IDAuNywgDQogICAgICAgICAgICAgICAgICAgc2Vucy53ID0gMC43NSwNCiAgICAgICAgICAgICAgICAgICBkZWNpc2lvbi5sYWJlbHMgPSBjKCJEaWRuJ3QgcHVyY2hhc2UiLCJQdXJjaGFzZWQiKSwNCiAgICAgICAgICBwcm9ncmVzcyA9IEZBTFNFKSAtPg0KICBpbml0aWFsX3RyZWUNCg0KcGxvdChpbml0aWFsX3RyZWUpDQpgYGANCg0KVGhpcyBpcyBhIHNpbXBsZSBtb2RlbCB0aGF0IGNvcnJlY3RseSBwcmVkaWN0cyBhIGdvb2QgcHJvcG9ydGlvbiBvZiB0aGUgY3VzdG9tZXJzIHRoYXQgcHVyY2hhc2VkLiAgDQpgYGB7cn0NCmluaXRpYWxfdHJlZQ0KYGBgDQoNClRoaXMgbW9kZWwgaGVhdmlseSBlbXBoYXNpc2VkIGNvcnJlY3RseSBpZGVudGlmeWluZyB0aGUgcGVvcGxlIHdobyB3b3VsZCBwdXJjaGFzZSBhdCB0aGUgZXhwZW5zZSBvZiBpbmNsdWRpbmcgbW9yZSBwZW9wbGUgd2hvIHdvdWxkbid0IHB1cmNoYXNlLiBXZSBnZXQgYSBnZXQgYSBoaWdoIGRlZ3JlZSBvZiBzZW5zaXRpdml0eSAoY29ycmVjdGx5IHByZWRpY3RpbmcgdGhvc2Ugd2hvIHB1cmNoYXNlZCkgZnJvbSB0aGUgbWFpbGNoaW1wIGVuZ2FnZW1lbnQgZmlndXJlLCBwZW9wbGUgdGlja2luZyB3aGF0IHR5cGVzIG9mIGVtYWlscyB0aGV5IHdhbnQgdG8gcmVjZWl2ZSBhbmQgdGhlbSBub3Qgd2FudGluZyB0aGUgTW9uZGF5IGVtYWlsLg0KDQpUaGlzIG1vZGVsIHdpbGwgYmUgb3VyIGJhc2VsaW5lIHdoZW4gd2UgZ2V0IGludG8gYnVpbGRpbmcgbW9yZSBtb2RlbHMuDQoNCiMgT3BlbnMNCg0KTm93IHRoYXQgd2UgaGF2ZSBmb3IgZWFjaCBwZXJzb24gb24gdGhlIG1haWxpbmcgbGlzdCwgd2hldGhlciB0aGV5IG1hZGUgYSBwdXJjaGFzZSBvciBub3QsIHdlIHNob3VsZCBidWlsZCBzb21lIGZlYXR1cmVzIGFyb3VuZCBob3cgb2Z0ZW4gcGVvcGxlIGhhdmUgb3BlbmVkIHRoZSBuZXdzbGV0dGVycyBpbiB0aGUgcGFzdC4NCg0KYGBge3J9DQpsaWJyYXJ5KHBhZHIpDQpsaWJyYXJ5KHRpbWVEYXRlKQ0KDQptYWlsY2hpbXBfbGFiZWxsZWQgJT4lIA0KICB0aGlja2VuKCJ3ZWVrIiwic3RhcnRfd2VlayIsYnk9ImNvbmZpcm1fdGltZSIpICAlPiUgDQogIG11dGF0ZShlYXJsaWVzdD1jb2FsZXNjZShlYXJsaWVzdCwgYXMuUE9TSVhjdCgiMjAxNy0xMi0zMSIpKSkgJT4lICANCiAgdGhpY2tlbigid2VlayIsInNhbGVfd2VlayIsYnk9ImVhcmxpZXN0IikgJT4lDQogIHNlbGVjdChlbWFpbF91c2VyX2lkLCBzdGFydF93ZWVrLCBzYWxlX3dlZWspIC0+DQogIGNoaW1wX2xpdGUNCg0KDQpvcGVucyA8LSBkYkdldFF1ZXJ5KGNvbiwNCiAgICAic2VsZWN0IA0KICAgIGVtYWlsX3VzZXJfaWQsIA0KICAgIENPTlZFUlQoZGF0ZSwgby5jbGlja190aW1lc3RhbXApIGFzIGNsaWNrX3RpbWVzdGFtcCwNCiAgICBjb3VudCgqKSBhcyBuDQogICAgDQogICAgZnJvbSBhbm9uLm1haWxjaGltcF9vcGVucyBvDQogICAgDQogICAgZ3JvdXAgYnkgZW1haWxfdXNlcl9pZCwgDQogICAgQ09OVkVSVChkYXRlLCBvLmNsaWNrX3RpbWVzdGFtcCkiKQ0KDQphcy5EYXRlKCIyMDExLTAxLTAxIikgJT4lIA0KICBzZXEuRGF0ZShhcy5EYXRlKCIyMDE4LTAxLTAxIiksIDEpIC0+DQogIGRhdGVzX3NlcQ0KDQpkYXRlc19zZXEgJT4lIA0KICBmb3JtYXQoKSAlPiUgDQogIHRpbWVEYXRlKCkgLT4NCiAgZGF0ZXNfdGltZURhdGUNCg0KaG9saWRheTwtaXNIb2xpZGF5KGRhdGVzX3RpbWVEYXRlLGhvbGlkYXlOWVNFKDIwMTE6MjAxOCkpDQp3ZWVrZGF5PC1pc1dlZWtkYXkoZGF0ZXNfdGltZURhdGUpDQoNCmRhdGVzX2xvb2t1cDwtdGliYmxlKGNsaWNrX3RpbWVzdGFtcD1kYXRlc19zZXEsDQogICAgICAgICAgICAgICAgICAgICBob2xpZGF5LA0KICAgICAgICAgICAgICAgICAgICAgd2Vla2RheSwNCiAgICAgICAgICAgICAgICAgICAgIHdlZWtlbmQ9IXdlZWtkYXkpDQpvcGVucyAlPiUgDQogIGxlZnRfam9pbihkYXRlc19sb29rdXApLT4NCiAgb3BlbnMNCg0Kb3BlbnMgJT4lIA0KICB0aGlja2VuKCJ3ZWVrIiwgImNsaWNrIikgJT4lDQogIGlubmVyX2pvaW4oY2hpbXBfbGl0ZSwgYnk9ImVtYWlsX3VzZXJfaWQiKSAlPiUgDQogIG11dGF0ZShzdGFydF93ZWVrX2RpZmY9ZGlmZnRpbWUoY2xpY2ssIHN0YXJ0X3dlZWssIHVuaXRzPSJ3ZWVrIiksDQogICAgICAgICBzYWxlX3dlZWtfZGlmZj1kaWZmdGltZShzYWxlX3dlZWssIGNsaWNrLCB1bml0cz0id2VlayIpKSAlPiUgDQogIG11dGF0ZShzdGFydF9tb250aD0oYXMuaW50ZWdlcihzdGFydF93ZWVrX2RpZmYpICUvJSA0KSArMSwNCiAgICAgICAgIHNhbGVfbW9udGg9KGFzLmludGVnZXIoc2FsZV93ZWVrX2RpZmYpICUvJSA0KSArMSApICAtPg0KICBvcGVucw0KDQpvcGVucyAlPiUgDQogIHNhbXBsZV9uKDIwMCkNCmBgYA0KDQojIyBPcGVucyBieSB1c2FnZQ0KYGBge3J9DQoob3BlbnMgJT4lIA0KICBncm91cF9ieShlbWFpbF91c2VyX2lkKSAlPiUgDQogIHN1bW1hcmlzZShob2xpZGF5X29wZW5fcmF3PXN1bShob2xpZGF5Km4pLA0KICAgICAgICAgICAgaG9saWRheV9vcGVuX3Byb3A9aG9saWRheV9vcGVuX3Jhdy9zdW0obiksDQogICAgICAgICAgICB3ZWVrZGF5X29wZW5fcmF3PXN1bSh3ZWVrZGF5Km4pLA0KICAgICAgICAgICAgd2Vla2RheV9vcGVuX3Byb3A9d2Vla2RheV9vcGVuX3Jhdy9zdW0obiksDQogICAgICAgICAgICB3ZWVrZW5kX29wZW5fcmF3PXN1bSh3ZWVrZW5kKm4pLA0KICAgICAgICAgICAgd2Vla2VuZF9vcGVuX3Byb3A9d2Vla2VuZF9vcGVuX3Jhdy9zdW0obikNCiAgICAgICAgICAgICkgIC0+DQogIG9wZW5zX3R5cGVvZmRheSkNCmBgYA0KDQojIyBPcGVucyBvdmVyIHRpbWUNCg0KYGBge3J9DQpvcGVucyAlPiUgDQogIGZpbHRlcihzdGFydF9tb250aCAgPiAwKSAlPiUgDQogIG11dGF0ZShzaW5jZV9zdGFydF9oPXN0YXJ0X21vbnRoICUvJSA2KSAlPiUgDQogIGdyb3VwX2J5KGVtYWlsX3VzZXJfaWQsc2luY2Vfc3RhcnRfaCkgJT4lIA0KICBzdW1tYXJpc2Uob3BlbnM9c3VtKG4pLGFjdGl2ZT1UUlVFKSAlPiUgDQogIGdyb3VwX2J5KGVtYWlsX3VzZXJfaWQpICU+JSANCiAgbXV0YXRlKG9wZW5fcHJvcD1vcGVucy9zdW0ob3BlbnMpLA0KICAgICAgICAgc2luY2Vfc3RhcnRfaD1wYXN0ZTAoImgiLHNpbmNlX3N0YXJ0X2gpKSAtPg0KICBvcGVuc19zdGFydF9obA0KDQpvcGVuc19zdGFydF9obCAlPiUgDQogIHNlbGVjdChlbWFpbF91c2VyX2lkOnNpbmNlX3N0YXJ0X2gsIG9wZW5zKSAlPiUgDQogIG11dGF0ZShzaW5jZV9zdGFydF9oPXBhc3RlMChzaW5jZV9zdGFydF9oLCJfb3BlbnMiKSkgJT4lIA0KICBzcHJlYWQoc2luY2Vfc3RhcnRfaCwgb3BlbnMsIGZpbGw9MCkgLT4NCiAgb3BlbnNfc3RhcnRfb3BlbnMNCg0Kb3BlbnNfc3RhcnRfaGwgJT4lIA0KICBzZWxlY3QoZW1haWxfdXNlcl9pZDpzaW5jZV9zdGFydF9oLCBhY3RpdmUpICU+JSANCiAgbXV0YXRlKHNpbmNlX3N0YXJ0X2g9cGFzdGUwKHNpbmNlX3N0YXJ0X2gsIl9vcGVuc2FjdGl2ZSIpKSAlPiUgDQogIHNwcmVhZChzaW5jZV9zdGFydF9oLCBhY3RpdmUsIGZpbGw9RkFMU0UpIC0+DQogIG9wZW5zX3N0YXJ0X2FjdGl2ZQ0KDQpvcGVuc19zdGFydF9obCAlPiUgDQogIHNlbGVjdChlbWFpbF91c2VyX2lkOnNpbmNlX3N0YXJ0X2gsIG9wZW5fcHJvcCkgJT4lIA0KICBtdXRhdGUoc2luY2Vfc3RhcnRfaD1wYXN0ZTAoc2luY2Vfc3RhcnRfaCwiX29wZW5zcHJvcCIpKSAlPiUgDQogIHNwcmVhZChzaW5jZV9zdGFydF9oLCBvcGVuX3Byb3AsIGZpbGw9MCkgLT4NCiAgb3BlbnNfc3RhcnRfcHJvcA0KDQoob3BlbnNfc3RhcnRfb3BlbnMgJT4lIA0KICBsZWZ0X2pvaW4ob3BlbnNfc3RhcnRfYWN0aXZlKSAlPiUgDQogIGxlZnRfam9pbihvcGVuc19zdGFydF9wcm9wKSAtPg0KICBvcGVuc19hY3Rpdml0eSkNCg0KYGBgDQoNCg0KDQpgYGB7ciBjb2xsYXBzZT1UUlVFfQ0Kcm0obGlzdD1jKCJvcGVucyIsIm9wZW5zX3N0YXJ0X2hsIiwNCiAgICAgICAgICAib3BlbnNfc3RhcnRfb3BlbnMiLCJvcGVuc19zdGFydF9hY3RpdmUiLA0KICAgICAgICAgICJvcGVuc19zdGFydF9wcm9wIikpDQoNCmBgYA0KDQojIENsaWNrcw0KYGBge3J9DQpjbGlja3MgPC0gZGJHZXRRdWVyeShjb24sDQogICAgInNlbGVjdCANCiAgICBlbWFpbF91c2VyX2lkLCANCiAgICBDT05WRVJUKGRhdGUsIG8uY2xpY2tfdGltZXN0YW1wKSBhcyBjbGlja190aW1lc3RhbXAsDQogICAgby5jbGlja191cmwsDQogICAgY291bnQoKikgYXMgbg0KICAgIA0KICAgIGZyb20gYW5vbi5tYWlsY2hpbXBfY2xpY2tzIG8NCiAgICANCiAgICBncm91cCBieSBlbWFpbF91c2VyX2lkLCANCiAgICBDT05WRVJUKGRhdGUsIG8uY2xpY2tfdGltZXN0YW1wKSwNCiAgICBvLmNsaWNrX3VybCIpDQoNCg0KY2xpY2tzICU+JSANCiAgbGVmdF9qb2luKGRhdGVzX2xvb2t1cCktPg0KICBjbGlja3MNCg0KY2xpY2tzICU+JSANCiAgdGhpY2tlbigid2VlayIsICJjbGljayIpICU+JQ0KICBpbm5lcl9qb2luKGNoaW1wX2xpdGUsIGJ5PSJlbWFpbF91c2VyX2lkIikgJT4lIA0KICBtdXRhdGUoc3RhcnRfd2Vla19kaWZmPWRpZmZ0aW1lKGNsaWNrLCBzdGFydF93ZWVrLCB1bml0cz0id2VlayIpLA0KICAgICAgICAgc2FsZV93ZWVrX2RpZmY9ZGlmZnRpbWUoc2FsZV93ZWVrLCBjbGljaywgdW5pdHM9IndlZWsiKSkgJT4lIA0KICBtdXRhdGUoc3RhcnRfbW9udGg9KGFzLmludGVnZXIoc3RhcnRfd2Vla19kaWZmKSAlLyUgNCkgKzEsDQogICAgICAgICBzYWxlX21vbnRoPShhcy5pbnRlZ2VyKHNhbGVfd2Vla19kaWZmKSAlLyUgNCkgKzEgKSAgLT4NCiAgY2xpY2tzDQoNCmBgYA0KDQoNCiMjIENsaWNrcyBieSB1c2FnZQ0KYGBge3J9DQooY2xpY2tzICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCkgJT4lIA0KICBzdW1tYXJpc2UoaG9saWRheV9jbGlja3NfcmF3PXN1bShob2xpZGF5Km4pLA0KICAgICAgICAgICAgaG9saWRheV9jbGlja3NfcHJvcD1ob2xpZGF5X2NsaWNrc19yYXcvc3VtKG4pLA0KICAgICAgICAgICAgd2Vla2RheV9jbGlja3NfcmF3PXN1bSh3ZWVrZGF5Km4pLA0KICAgICAgICAgICAgd2Vla2RheV9jbGlja3NfcHJvcD13ZWVrZGF5X2NsaWNrc19yYXcvc3VtKG4pLA0KICAgICAgICAgICAgd2Vla2VuZF9jbGlja3NfcmF3PXN1bSh3ZWVrZW5kKm4pLA0KICAgICAgICAgICAgd2Vla2VuZF9jbGlja3NfcHJvcD13ZWVrZW5kX2NsaWNrc19yYXcvc3VtKG4pDQogICAgICAgICAgICApICAtPg0KICBjbGlja3NfdHlwZW9mZGF5KQ0KYGBgDQoNCiMjIENsaWNrcyBvdmVyIHRpbWUNCg0KYGBge3J9DQpjbGlja3MgJT4lIA0KICBmaWx0ZXIoc3RhcnRfbW9udGggID4gMCkgJT4lIA0KICBtdXRhdGUoc2luY2Vfc3RhcnRfaD1zdGFydF9tb250aCAlLyUgNikgJT4lIA0KICBncm91cF9ieShlbWFpbF91c2VyX2lkLHNpbmNlX3N0YXJ0X2gpICU+JSANCiAgc3VtbWFyaXNlKGNsaWNrcz1zdW0obiksYWN0aXZlPVRSVUUpICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCkgJT4lIA0KICBtdXRhdGUoY2xpY2tfcHJvcD1jbGlja3Mvc3VtKGNsaWNrcyksDQogICAgICAgICBzaW5jZV9zdGFydF9oPXBhc3RlMCgiaCIsc2luY2Vfc3RhcnRfaCkpIC0+DQogIGNsaWNrc19zdGFydF9obA0KDQpjbGlja3Nfc3RhcnRfaGwgJT4lIA0KICBzZWxlY3QoZW1haWxfdXNlcl9pZDpzaW5jZV9zdGFydF9oLCBjbGlja3MpICU+JSANCiAgbXV0YXRlKHNpbmNlX3N0YXJ0X2g9cGFzdGUwKHNpbmNlX3N0YXJ0X2gsIl9jbGlja3MiKSkgJT4lIA0KICBzcHJlYWQoc2luY2Vfc3RhcnRfaCwgY2xpY2tzLCBmaWxsPTApIC0+DQogIGNsaWNrc19zdGFydF9jbGlja3MNCg0KY2xpY2tzX3N0YXJ0X2hsICU+JSANCiAgc2VsZWN0KGVtYWlsX3VzZXJfaWQ6c2luY2Vfc3RhcnRfaCwgYWN0aXZlKSAlPiUgDQogIG11dGF0ZShzaW5jZV9zdGFydF9oPXBhc3RlMChzaW5jZV9zdGFydF9oLCJfY2xpY2thY3RpdmUiKSkgJT4lIA0KICBzcHJlYWQoc2luY2Vfc3RhcnRfaCwgYWN0aXZlLCBmaWxsPUZBTFNFKSAtPg0KICBjbGlja3Nfc3RhcnRfYWN0aXZlDQoNCmNsaWNrc19zdGFydF9obCAlPiUgDQogIHNlbGVjdChlbWFpbF91c2VyX2lkOnNpbmNlX3N0YXJ0X2gsIGNsaWNrX3Byb3ApICU+JSANCiAgbXV0YXRlKHNpbmNlX3N0YXJ0X2g9cGFzdGUwKHNpbmNlX3N0YXJ0X2gsIl9jbGlja3Byb3AiKSkgJT4lIA0KICBzcHJlYWQoc2luY2Vfc3RhcnRfaCwgY2xpY2tfcHJvcCwgZmlsbD0wKSAtPg0KICBjbGlja3Nfc3RhcnRfcHJvcA0KDQooY2xpY2tzX3N0YXJ0X2NsaWNrcyAlPiUgDQogIGxlZnRfam9pbihjbGlja3Nfc3RhcnRfYWN0aXZlKSAlPiUgDQogIGxlZnRfam9pbihjbGlja3Nfc3RhcnRfcHJvcCkgLT4NCiAgY2xpY2tfYWN0aXZpdHkpDQoNCmBgYA0KDQoNCg0KYGBge3IgY29sbGFwc2U9VFJVRX0NCnJtKGxpc3Q9YygiY2xpY2tzX3N0YXJ0X2hsIiwNCiAgICAgICAgICAiY2xpY2tzX3N0YXJ0X2NsaWNrcyIsImNsaWNrc19zdGFydF9hY3RpdmUiLA0KICAgICAgICAgICJjbGlja3Nfc3RhcnRfcHJvcCIpKQ0KDQpgYGANCg0KIyMgQ2xpY2tzIGJ5IHRvcGljDQoNCkZvciBob3cgd2UgYXJyaXZlZCBhdCB0b3BpY3MgcGVyIHVybCwgY2hlY2sgb3V0IFtXaGF0IHRvcGljcyBkaWQgcGVvcGxlIGNsaWNrP10oYnJlbnRzYml6SW50ZXJuZXQubmIuaHRtbCkNCg0KYGBge3J9DQpjbGlja3MgJT4lIA0KICBpbm5lcl9qb2luKHsNCiAgICAiLi4vZGF0YS9saW5rX2xvb2t1cC5jc3YiICU+JSANCiAgICByZWFkX2NzdigpDQogIH0sIGJ5PSJjbGlja191cmwiKSAlPiUgDQogIGxlZnRfam9pbih7DQogICAgIi4uL2RhdGEvdXJsdG9waWNzLmNzdiIlPiUgDQogICAgcmVhZF9jc3YoKQ0KICB9LCBieT1jKCJjbGVhbl9maWxlIj0idXJsIikpICU+JSANCiAgbXV0YXRlKGJyZW50PWlmZWxzZShzdHJfZGV0ZWN0KGNsZWFuX3VybCwiYnJlbnRvemFyIiksIkIiLCJuQiIpKSAlPiUgDQogIGNvdW50KCBlbWFpbF91c2VyX2lkLCBicmVudCwgdG9waWMpICU+JSANCiAgdW5pdGUodG9waWNfYnJlbnQsdG9waWMsIGJyZW50KSAlPiUgDQogIG11dGF0ZSh0b3BpY19icmVudD1wYXN0ZTAoInRvcGljXyIsdG9waWNfYnJlbnQpKSAlPiUgDQogIGdyb3VwX2J5KGVtYWlsX3VzZXJfaWQpICU+JSANCiAgbXV0YXRlKHByb3A9bm4vc3VtKG5uKSkgLT4NCiAgdG9waWNfY2xpY2tzX2wNCg0KdG9waWNfY2xpY2tzX2wgJT4lIA0KICBzZWxlY3QoLXByb3ApICU+JSANCiAgbXV0YXRlKHRvcGljX2JyZW50PXBhc3RlMCh0b3BpY19icmVudCwiX2NsaWNrcyIpKSAlPiUgDQogIHNwcmVhZCh0b3BpY19icmVudCwgbm4sIGZpbGw9MCkgLT4NCiAgdG9waWNfY2xpY2tzX2NsaWNrcw0KDQp0b3BpY19jbGlja3NfbCAlPiUgDQogIHNlbGVjdCgtbm4pICU+JSANCiAgbXV0YXRlKHRvcGljX2JyZW50PXBhc3RlMCh0b3BpY19icmVudCwiX3Byb3AiKSkgJT4lIA0KICBzcHJlYWQodG9waWNfYnJlbnQsIHByb3AsIGZpbGw9MCkgLT4NCiAgdG9waWNfY2xpY2tzX3Byb3ANCg0KdG9waWNfY2xpY2tzX2wgJT4lIA0KICBtdXRhdGUodG9waWNfYnJlbnQ9cGFzdGUwKHRvcGljX2JyZW50LCJfYWN0aXZlIikpICU+JSANCiAgZ3JvdXBfYnkodG9waWNfYnJlbnQsZW1haWxfdXNlcl9pZCkgJT4lIA0KICBzdW1tYXJpc2UoYWN0aXZlPTEpICU+JSANCiAgc3ByZWFkKHRvcGljX2JyZW50LCBhY3RpdmUsIGZpbGw9MCkgLT4NCiAgdG9waWNfY2xpY2tzX2FjdGl2ZQ0KDQoodG9waWNfY2xpY2tzX2NsaWNrcyAlPiUgDQogIGlubmVyX2pvaW4odG9waWNfY2xpY2tzX3Byb3ApICU+JSANCiAgaW5uZXJfam9pbih0b3BpY19jbGlja3NfYWN0aXZlKS0+DQogIHRvcGljX2NsaWNrcykNCmBgYA0KDQoNCmBgYHtyIGNvbGxhcHNlPVRSVUV9DQpybShsaXN0PWMoInRvcGljc19jbGlja3NfbCIsDQogICAgICAgICAgInRvcGljX2NsaWNrc19jbGlja3MiLCJ0b3BpY19jbGlja3NfcHJvcCIsDQogICAgICAgICAgInRvcGljX2NsaWNrc19hY3RpdmUiLA0KICAgICAgICAgICJjbGlja3MiKSkNCg0KYGBgDQoNCg0KIyBQdXR0aW5nIGl0IGFsbCB0b2dldGhlcg0KDQpgYGB7cn0NCm1haWxjaGltcF9sYWJlbGxlZCAlPiUgDQogIGxlZnRfam9pbihvcGVuc190eXBlb2ZkYXksIGJ5PSJlbWFpbF91c2VyX2lkIikgJT4lIA0KICBsZWZ0X2pvaW4ob3BlbnNfYWN0aXZpdHksIGJ5PSJlbWFpbF91c2VyX2lkIikgJT4lIA0KICBsZWZ0X2pvaW4oY2xpY2tzX3R5cGVvZmRheSwgYnk9ImVtYWlsX3VzZXJfaWQiKSAlPiUgDQogIGxlZnRfam9pbihjbGlja19hY3Rpdml0eSwgYnk9ImVtYWlsX3VzZXJfaWQiKSAlPiUgDQogIGxlZnRfam9pbih0b3BpY19jbGlja3MsIGJ5PSJlbWFpbF91c2VyX2lkIikgLT4gDQogIG1haWxjaGltcF93aWRlDQoNCm1haWxjaGltcF93aWRlICU+JSANCiAgc2VsZWN0KC0oZW1haWxfZG9tYWluX2lkOmNsaWVudF90eXBlKSwtKG9wdGluX3RpbWU6dGltZV96b25lKSwNCiAgICAgICAgIC0ocmVnaW9uOm5vdGVzKSkgJT4lIA0KICBtdXRhdGUobmV2ZXJfb3BlbmVkPWlzLm5hKGhvbGlkYXlfb3Blbl9yYXcpLA0KICAgICAgICAgbmV2ZXJfY2xpY2tlZD1pcy5uYShob2xpZGF5X2NsaWNrc19yYXcpKSAtPiANCiAgbWFpbGNoaW1wX3dpZGUNCg0KbWFpbGNoaW1wX3dpZGU8LW11dGF0ZV9pZihtYWlsY2hpbXBfd2lkZSwgaXMubG9naWNhbCwgfmNvYWxlc2NlKC4sRkFMU0UpKQ0KbWFpbGNoaW1wX3dpZGU8LW11dGF0ZV9pZihtYWlsY2hpbXBfd2lkZSwgaXNfZG91YmxlLCB+Y29hbGVzY2UoLiwwKSkNCm1haWxjaGltcF93aWRlPC1tdXRhdGVfaWYobWFpbGNoaW1wX3dpZGUsIGlzX2ludGVnZXIsIH5jb2FsZXNjZSguLDBMKSkgDQoNCndyaXRlX2NzdihtYWlsY2hpbXBfd2lkZSwiLi4vb3V0cHV0cy9maW5hbGRhdGFzZXQuY3N2IikNCg0KbWFpbGNoaW1wX3dpZGUNCg0KYGBgDQo=